package sf.lambda;

import sf.common.exception.SmallOrmException;
import sf.core.DBField;
import sf.core.DBObject;
import sf.database.dialect.DBDialect;
import sf.database.meta.ColumnMapping;
import sf.database.meta.MetaHolder;
import sf.database.meta.TableMapping;
import sf.tools.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 通用的Example查询对象
 */
public class LambdaExample {
    protected String orderByClause;

    protected boolean distinct;

    protected boolean exists;

    protected boolean notNull;

    protected boolean forUpdate;

    //查询字段
    protected Set<String> selectColumns;

    //排除的查询字段
    protected Set<String> excludeColumns;

    protected String countColumn;

    protected List<Criteria> oredCriteria;

    protected Class<?> entityClass;

    protected TableMapping table;
    //属性和列对应
    protected Map<DBField, ColumnMapping> propertyMap;
    //动态表名
    protected String tableName;

    protected OrderBy ORDERBY;

    protected DBDialect dialect;

    /**
     * 默认exists为true
     * @param entityClass
     */
    public LambdaExample(Class<? extends DBObject> entityClass) {
        this(entityClass, true);
    }

    /**
     * 带exists参数的构造方法，默认notNull为false，允许为空
     * @param entityClass
     * @param exists      - true时，如果字段不存在就抛出异常，false时，如果不存在就不使用该字段的条件
     */
    public LambdaExample(Class<? extends DBObject> entityClass, boolean exists) {
        this(entityClass, exists, false);
    }

    /**
     * 带exists参数的构造方法
     * @param entityClass
     * @param exists      - true时，如果字段不存在就抛出异常，false时，如果不存在就不使用该字段的条件
     * @param notNull     - true时，如果值为空，就会抛出异常，false时，如果为空就不使用该字段的条件
     */
    public LambdaExample(Class<? extends DBObject> entityClass, boolean exists, boolean notNull) {
        this.exists = exists;
        this.notNull = notNull;
        oredCriteria = new ArrayList<Criteria>();
        this.entityClass = entityClass;
        table = MetaHolder.getMeta(entityClass);
        //根据李领北建议修改#159
        propertyMap = table.getSchemaMap();
        this.ORDERBY = new OrderBy(this, propertyMap);
    }


    private LambdaExample(Builder builder) {
        this.exists = builder.exists;
        this.notNull = builder.notNull;
        this.distinct = builder.distinct;
        this.entityClass = builder.entityClass;
        this.propertyMap = builder.propertyMap;
        this.selectColumns = builder.selectColumns;
        this.excludeColumns = builder.excludeColumns;
        this.oredCriteria = builder.exampleCriterias;
        this.forUpdate = builder.forUpdate;
        this.tableName = builder.tableName;
        this.dialect = builder.dialect;

        if (!StringUtils.isEmpty(builder.orderByClause.toString())) {
            this.orderByClause = builder.orderByClause.toString();
        }
    }

    public static Builder builder(Class<? extends DBObject> entityClass) {
        return new Builder(entityClass);
    }

    public OrderBy orderBy(DBField property) {
        this.ORDERBY.orderBy(property);
        return this.ORDERBY;
    }

    /**
     * 排除查询字段，优先级低于 selectProperties
     * @param properties 属性名的可变参数
     * @return
     */
    public LambdaExample excludeProperties(DBField... properties) {
        if (properties != null && properties.length > 0) {
            if (this.excludeColumns == null) {
                this.excludeColumns = new LinkedHashSet<String>();
            }
            for (DBField property : properties) {
                if (propertyMap.containsKey(property)) {
                    this.excludeColumns.add(propertyMap.get(property).getRawColumnName());
                } else {
                    throw new SmallOrmException("类 " + entityClass.getSimpleName() + " 不包含属性 \'" + property + "\'，或该属性被@Transient注释！");
                }
            }
        }
        return this;
    }

    /**
     * 指定要查询的属性列 - 这里会自动映射到表字段
     * @param properties
     * @return
     */
    public LambdaExample selectProperties(DBField... properties) {
        if (properties != null && properties.length > 0) {
            if (this.selectColumns == null) {
                this.selectColumns = new LinkedHashSet<String>();
            }
            for (DBField property : properties) {
                if (propertyMap.containsKey(property)) {
                    this.selectColumns.add(propertyMap.get(property).getRawColumnName());
                } else {
                    throw new SmallOrmException("类 " + entityClass.getSimpleName() + " 不包含属性 \'" + property + "\'，或该属性被@Transient注释！");
                }
            }
        }
        return this;
    }

    public void or(Criteria criteria) {
        criteria.setAndOr("or");
        oredCriteria.add(criteria);
    }

    public Criteria or() {
        Criteria criteria = createCriteriaInternal();
        criteria.setAndOr("or");
        oredCriteria.add(criteria);
        return criteria;
    }

    public void and(Criteria criteria) {
        criteria.setAndOr("and");
        oredCriteria.add(criteria);
    }

    public Criteria and() {
        Criteria criteria = createCriteriaInternal();
        criteria.setAndOr("and");
        oredCriteria.add(criteria);
        return criteria;
    }

    public Criteria createCriteria() {
        Criteria criteria = createCriteriaInternal();
        if (oredCriteria.size() == 0) {
            criteria.setAndOr("and");
            oredCriteria.add(criteria);
        }
        return criteria;
    }

    protected Criteria createCriteriaInternal() {
        Criteria criteria = new Criteria(dialect, propertyMap, exists, notNull);
        return criteria;
    }

    public void clear() {
        oredCriteria.clear();
        orderByClause = null;
        distinct = false;
    }

    public static class OrderBy {
        //属性和列对应
        protected Map<DBField, ColumnMapping> propertyMap;
        private LambdaExample example;
        private Boolean isProperty;

        public OrderBy(LambdaExample example, Map<DBField, ColumnMapping> propertyMap) {
            this.example = example;
            this.propertyMap = propertyMap;
        }

        private String property(DBField property) {
            if (property == null) {
                throw new SmallOrmException("接收的property为空！");
            }
            if (!propertyMap.containsKey(property)) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            }
            return propertyMap.get(property).getRawColumnName();
        }

        public OrderBy orderBy(DBField property) {
            String column = property(property);
            if (column == null) {
                isProperty = false;
                return this;
            }
            if (StringUtils.isNotEmpty(example.getOrderByClause())) {
                example.setOrderByClause(example.getOrderByClause() + "," + column);
            } else {
                example.setOrderByClause(column);
            }
            isProperty = true;
            return this;
        }

        public OrderBy desc() {
            if (isProperty) {
                example.setOrderByClause(example.getOrderByClause() + " DESC");
                isProperty = false;
            }
            return this;
        }

        public OrderBy asc() {
            if (isProperty) {
                example.setOrderByClause(example.getOrderByClause() + " ASC");
                isProperty = false;
            }
            return this;
        }


    }

    protected abstract static class GeneratedCriteria {
        protected List<Criterion> criteria;
        //字段是否必须存在
        protected boolean exists;
        //值是否不能为空
        protected boolean notNull;
        //连接条件
        protected String andOr;
        //属性和列对应
        protected Map<DBField, ColumnMapping> propertyMap;
        protected DBDialect dialect;

        protected GeneratedCriteria(DBDialect dialect, Map<DBField, ColumnMapping> propertyMap, boolean exists, boolean notNull) {
            super();
            this.exists = exists;
            this.notNull = notNull;
            criteria = new ArrayList<Criterion>();
            this.propertyMap = propertyMap;
            this.dialect = dialect;
        }

        private String column(DBField property) {
            if (propertyMap.containsKey(property)) {
                String name = propertyMap.get(property).getRawColumnName();
                if (dialect != null && name != null) {
                    name = dialect.wrapKeyword(name);
                }
                return name;
            } else if (exists) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            } else {
                return null;
            }
        }

        private ColumnMapping columnMapping(DBField property) {
            if (propertyMap.containsKey(property)) {
                return propertyMap.get(property);
            } else if (exists) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            } else {
                return null;
            }
        }

        private DBField property(DBField property) {
            if (propertyMap.containsKey(property)) {
                return property;
            } else if (exists) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            } else {
                return null;
            }
        }

        protected void addCriterion(String condition) {
            if (condition == null) {
                throw new SmallOrmException("Value for condition cannot be null");
            }
            if (condition.startsWith("null")) {
                return;
            }
            criteria.add(new Criterion(condition));
        }

        protected void addCriterion(String condition, Object value, DBField property) {
            if (value == null) {
                if (notNull) {
                    throw new SmallOrmException("Value for " + property + " cannot be null");
                } else {
                    return;
                }
            }
            if (property == null) {
                return;
            }
            criteria.add(new Criterion(condition, value, columnMapping(property)));
        }

        protected void addCriterion(String condition, Object value1, Object value2, DBField property) {
            if (value1 == null || value2 == null) {
                if (notNull) {
                    throw new SmallOrmException("Between values for " + property + " cannot be null");
                } else {
                    return;
                }
            }
            if (property == null) {
                return;
            }
            criteria.add(new Criterion(condition, value1, value2, columnMapping(property)));
        }

        public Criteria isNull(DBField property) {
            addCriterion(column(property) + " is null");
            return (Criteria) this;
        }

        public Criteria isNotNull(DBField property) {
            addCriterion(column(property) + " is not null");
            return (Criteria) this;
        }

        public Criteria eq(DBField property, Object value) {
            addCriterion(column(property) + " =?", value, property(property));
            return (Criteria) this;
        }

        public Criteria ne(DBField property, Object value) {
            addCriterion(column(property) + " <>?", value, property(property));
            return (Criteria) this;
        }

        public Criteria gt(DBField property, Object value) {
            addCriterion(column(property) + " >?", value, property(property));
            return (Criteria) this;
        }

        public Criteria ge(DBField property, Object value) {
            addCriterion(column(property) + " >= ?", value, property(property));
            return (Criteria) this;
        }

        public Criteria lt(DBField property, Object value) {
            addCriterion(column(property) + " <?", value, property(property));
            return (Criteria) this;
        }

        public Criteria le(DBField property, Object value) {
            addCriterion(column(property) + " <=? ", value, property(property));
            return (Criteria) this;
        }

        public Criteria in(DBField property, Collection<?> values) {
            addCriterion(column(property) + " in" + toIn(values), values, property(property));
            return (Criteria) this;
        }

        protected String toIn(Collection<?> values) {
            StringBuilder sb = new StringBuilder();
            if (values != null) {
                sb.append("(");
                for (int i = 0; i < values.size(); i++) {
                    sb.append(i > 0 ? "," : "").append("?");
                }
                sb.append(") ");
            }
            return sb.toString();
        }

        public Criteria notIn(DBField property, Collection<?> values) {
            addCriterion(column(property) + " not in" + toIn(values), values, property(property));
            return (Criteria) this;
        }

        public Criteria between(DBField property, Object value1, Object value2) {
            addCriterion(column(property) + " between ? and ? ", value1, value2, property(property));
            return (Criteria) this;
        }

        public Criteria notBetween(DBField property, Object value1, Object value2) {
            addCriterion(column(property) + " not between ? and ?", value1, value2, property(property));
            return (Criteria) this;
        }

        public Criteria like(DBField property, String value) {
            addCriterion(column(property) + "  like ?", value, property(property));
            return (Criteria) this;
        }

        public Criteria like(DBField property, String value, char escape) {
            addCriterion(column(property) + "  like ? escape '" + escape + "'", value, property(property));
            return (Criteria) this;
        }

        public Criteria like(DBField property, String value, boolean escape) {
            addCriterion(column(property) + "  like ?" + (escape ? " escape '/'" : ""), value, property(property));
            return (Criteria) this;
        }

        public Criteria notLike(DBField property, String value) {
            addCriterion(column(property) + "  not like ?", value, property(property));
            return (Criteria) this;
        }

        public Criteria notLike(DBField property, String value, char escape) {
            addCriterion(column(property) + "  not like ? escape '" + escape + "'", value, property(property));
            return (Criteria) this;
        }

        public Criteria notLike(DBField property, String value, boolean escape) {
            addCriterion(column(property) + "  not like ?" + (escape ? " escape '/'" : ""), value, property(property));
            return (Criteria) this;
        }

        /**
         * 手写条件
         * @param condition 例如 "length(countryname)<5"
         * @return
         */
        public Criteria addCondition(String condition) {
            addCriterion(condition);
            return (Criteria) this;
        }

        /**
         * 手写左边条件，右边用value值
         * @param condition 例如 "length(countryname)="
         * @param value     例如 5
         * @return
         */
        public Criteria addCondition(String condition, DBField field, Object value) {
            criteria.add(new Criterion(condition, value, columnMapping(field)));
            return (Criteria) this;
        }

        public Criteria or() {
            addCondition(" or ");
            return (Criteria) this;
        }

        public Criteria and() {
            addCondition(" and ");
            return (Criteria) this;
        }

        public Criteria leftP() {
            addCondition("(");
            return (Criteria) this;
        }

        public Criteria rightP() {
            addCondition(")");
            return (Criteria) this;
        }

        public List<Criterion> getAllCriteria() {
            return criteria;
        }

        public String getAndOr() {
            return andOr;
        }

        public void setAndOr(String andOr) {
            this.andOr = andOr;
        }

        public List<Criterion> getCriteria() {
            return criteria;
        }

        public boolean isValid() {
            return criteria.size() > 0;
        }
    }

    public static class Criteria extends GeneratedCriteria {

        public Criteria(DBDialect dialect, Class<? extends DBObject> entityClass) {
            this(dialect, MetaHolder.getMeta(entityClass).getSchemaMap(), true, false);
        }

        protected Criteria(DBDialect dialect, Map<DBField, ColumnMapping> propertyMap, boolean exists, boolean notNull) {
            super(dialect, propertyMap, exists, notNull);
        }
    }

    public enum ValueType {
        noValue, singleValue, betweenValue, listValue
    }

    public static class Criterion {
        private String condition;

        private Object value;

        private Object secondValue;

        private ColumnMapping column;

        private ValueType valueType;

        protected Criterion(String condition) {
            super();
            this.condition = condition;
            this.valueType = ValueType.noValue;
        }

        protected Criterion(String condition, Object value, ColumnMapping column) {
            super();
            this.condition = condition;
            this.value = value;
            this.column = column;
            if (value instanceof Collection<?>) {
                this.valueType = ValueType.listValue;
            } else {
                this.valueType = ValueType.singleValue;
            }
        }

        protected Criterion(String condition, Object value, Object secondValue, ColumnMapping column) {
            super();
            this.condition = condition;
            this.value = value;
            this.secondValue = secondValue;
            this.column = column;
            this.valueType = ValueType.betweenValue;
        }

        public String getCondition() {
            return condition;
        }

        public Object getSecondValue() {
            return secondValue;
        }

        public ColumnMapping getColumn() {
            return column;
        }

        public Object getValue() {
            return value;
        }

        public ValueType getValueType() {
            return valueType;
        }
    }

    public static class Builder {
        private final Class<?> entityClass;
        protected TableMapping table;
        //属性和列对应
        protected Map<DBField, ColumnMapping> propertyMap;
        private StringBuilder orderByClause;
        private boolean distinct;
        private boolean exists;
        private boolean notNull;
        private boolean forUpdate;
        //查询字段
        private Set<String> selectColumns;
        //排除的查询字段
        private Set<String> excludeColumns;
        private String countColumn;
        private List<Criteria> sqlsCriteria;
        //动态表名
        private List<LambdaExample.Criteria> exampleCriterias;
        //动态表名
        private String tableName;
        private DBDialect dialect;

        public Builder(Class<? extends DBObject> entityClass) {
            this(entityClass, true);
        }

        public Builder(Class<? extends DBObject> entityClass, boolean exists) {
            this(entityClass, exists, false);
        }

        public Builder(Class<? extends DBObject> entityClass, boolean exists, boolean notNull) {
            this.entityClass = entityClass;
            this.exists = exists;
            this.notNull = notNull;
            this.orderByClause = new StringBuilder();
            this.table = MetaHolder.getMeta(entityClass);
            this.propertyMap = table.getSchemaMap();
            this.sqlsCriteria = new ArrayList<Criteria>(2);
        }

        public Builder distinct() {
            return setDistinct(true);
        }

        public Builder forUpdate() {
            return setForUpdate(true);
        }

        public Builder selectDistinct(DBField... properties) {
            select(properties);
            this.distinct = true;
            return this;
        }

        public Builder select(DBField... properties) {
            if (properties != null && properties.length > 0) {
                if (this.selectColumns == null) {
                    this.selectColumns = new LinkedHashSet<String>();
                }
                for (DBField property : properties) {
                    if (this.propertyMap.containsKey(property)) {
                        this.selectColumns.add(propertyMap.get(property).getRawColumnName());
                    } else {
                        throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
                    }
                }
            }
            return this;
        }

        public Builder notSelect(DBField... properties) {
            if (properties != null && properties.length > 0) {
                if (this.excludeColumns == null) {
                    this.excludeColumns = new LinkedHashSet<String>();
                }
                for (DBField property : properties) {
                    if (propertyMap.containsKey(property)) {
                        this.excludeColumns.add(propertyMap.get(property).getRawColumnName());
                    } else {
                        throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
                    }
                }
            }
            return this;
        }

        public Builder from(String tableName) {
            return setTableName(tableName);
        }

        public Builder where(LambdaSqls sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("and");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder where(LambdaSqlsCriteria sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("and");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder andWhere(LambdaSqls sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("and");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder andWhere(LambdaSqlsCriteria sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("and");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder orWhere(LambdaSqls sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("or");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder orWhere(LambdaSqlsCriteria sqls) {
            Criteria criteria = sqls.getCriteria();
            criteria.setAndOr("or");
            this.sqlsCriteria.add(criteria);
            return this;
        }

        public Builder orderBy(DBField... properties) {
            return orderByAsc(properties);
        }

        public Builder orderByAsc(DBField... properties) {
            contactOrderByClause(" Asc", properties);
            return this;
        }

        public Builder orderByDesc(DBField... properties) {
            contactOrderByClause(" Desc", properties);
            return this;
        }

        private void contactOrderByClause(String order, DBField... properties) {
            StringBuilder columns = new StringBuilder();
            for (DBField property : properties) {
                String column;
                if ((column = propertyforOderBy(property)) != null) {
                    columns.append(",").append(column);
                }
            }
            columns.append(order);
            if (columns.length() > 0) {
                orderByClause.append(columns);
            }
        }

        public LambdaExample build() {
            this.exampleCriterias = new ArrayList<Criteria>();
            for (Criteria criteria : sqlsCriteria) {
                LambdaExample.Criteria exampleCriteria = new LambdaExample.Criteria(dialect, this.propertyMap, this.exists, this.notNull);
                exampleCriteria.setAndOr(criteria.getAndOr());
                exampleCriteria.getAllCriteria().addAll(criteria.criteria);
                exampleCriterias.add(exampleCriteria);
            }

            if (this.orderByClause.length() > 0) {
                this.orderByClause = new StringBuilder(this.orderByClause.substring(1, this.orderByClause.length()));
            }

            return new LambdaExample(this);
        }

        private String column(DBField property) {
            if (propertyMap.containsKey(property)) {
                return propertyMap.get(property).getRawColumnName();
            } else if (exists) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            } else {
                return null;
            }
        }

        private DBField property(DBField property) {
            if (propertyMap.containsKey(property)) {
                return property;
            } else if (exists) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            } else {
                return null;
            }
        }

        private String propertyforOderBy(DBField property) {
            if (property == null) {
                throw new SmallOrmException("接收的property为空！");
            }
            if (!propertyMap.containsKey(property)) {
                throw new SmallOrmException("当前实体类不包含名为" + property + "的属性!");
            }
            return propertyMap.get(property).getRawColumnName();
        }

        public Builder setDistinct(boolean distinct) {
            this.distinct = distinct;
            return this;
        }

        public Builder setForUpdate(boolean forUpdate) {
            this.forUpdate = forUpdate;
            return this;
        }

        public Builder setTableName(String tableName) {
            this.tableName = tableName;
            return this;
        }

        public Builder setDialect(DBDialect dialect) {
            this.dialect = dialect;
            return this;
        }
    }

    public String getCountColumn() {
        return countColumn;
    }

    public String getDynamicTableName() {
        return tableName;
    }

    public Class<?> getEntityClass() {
        return entityClass;
    }

    public String getOrderByClause() {
        return orderByClause;
    }

    public void setOrderByClause(String orderByClause) {
        this.orderByClause = orderByClause;
    }

    public List<Criteria> getOredCriteria() {
        return oredCriteria;
    }

    public Set<String> getSelectColumns() {
        if (selectColumns != null && selectColumns.size() > 0) {
            //不需要处理
        } else if (excludeColumns != null && excludeColumns.size() > 0) {
            Collection<ColumnMapping> entityColumns = propertyMap.values();
            selectColumns = new LinkedHashSet<String>(entityColumns.size() - excludeColumns.size());
            for (ColumnMapping column : entityColumns) {
                if (!excludeColumns.contains(column.getColumn())) {
                    selectColumns.add(column.getRawColumnName());
                }
            }
        }
        return selectColumns;
    }

    public boolean isDistinct() {
        return distinct;
    }

    public void setDistinct(boolean distinct) {
        this.distinct = distinct;
    }

    public boolean isForUpdate() {
        return forUpdate;
    }

    public void setForUpdate(boolean forUpdate) {
        this.forUpdate = forUpdate;
    }

    /**
     * 指定 count(property) 查询属性
     * @param property
     */
    public void setCountProperty(DBField property) {
        if (propertyMap.containsKey(property)) {
            this.countColumn = propertyMap.get(property).getRawColumnName();
        }
    }

    /**
     * 设置表名
     * @param tableName
     */
    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public void setDialect(DBDialect dialect) {
        this.dialect = dialect;
    }
}